Step 1 - Define a data source

In this step of the tutorial you create a Kanzi Engine plugin in which you define a data source. You use the data source to provide the data to your application.

Assets for the tutorial

The starting point of this tutorial is stored in the <KanziWorkspace>/Tutorials/Data sources/Start directory:

The <KanziWorkspace>/Tutorials/Data sources/Completed directory contains the completed project of this tutorial.

Define a data source

To define a data source:

  1. In Visual Studio open the <KanziWorkspace>/Tutorials/Data sources/Start/Application/configs/platforms/win32/XML_data_source.sln Visual Studio solution.
    If you open the tutorial solution in Visual Studio 2017, when asked to retarget the project to the latest Microsoft toolset, click Cancel.
  2. Select one of the DLL solution configurations for your version of Visual Studio.
    During the development select one of the debug DLL configurations. When you are ready to create the version for production, select one of the release DLL configurations.
    For example, select the GL_vs2015_Release_DLL configuration.
  3. In Windows Explorer from the <KanziWorkspace>/Tutorials/Data sources/Assets/TinyXML-2 directory copy the tinyxml2.cpp and tinyxml2.h files to the <KanziWorkspace>/Tutorials/Data sources/Start/Application/src/plugin/src directory.
    In this tutorial you use the TinyXML-2 library to process the XML file which you use as a data source.
  4. In Visual Studio in the Solution Explorer right-click the XML_data_source project, select Add > Existing Item and in the <KanziWorkspace>/Tutorials/Data sources/Start/Application/src/plugin/src directory select the tinyxml2.cpp and tinyxml2.h files and click Add.
  5. In Visual Studio make these changes to the xmldatasource.hpp file:
    1. In the public section of the XMLDataSource class replace
      class XML_DATA_SOURCE_API XMLDataSource : public DataSource
      {
      public:
      
          KZ_METACLASS_BEGIN(XMLDataSource, DataSource, "CustomDataSourceType")
          KZ_METACLASS_END()
      
      ...
      
      };

      with this code to create a property type with which you can set the XML file you want this data source to use.
      class XML_DATA_SOURCE_API XMLDataSource : public DataSource
      {
      public:
          // Create the XmlFilename property type. You use this property type to tell the data source plugin which XML file to read.
          static PropertyType<string> XmlFilenameProperty;
      
          KZ_METACLASS_BEGIN(XMLDataSource, DataSource, "XML_data_source")
              // Add the property you created to the class metadata.
              KZ_METACLASS_PROPERTY_TYPE(XmlFilenameProperty)
          KZ_METACLASS_END()
      
      ...
      
      };
    2. In the public section of the XMLDataSource class add the functions for loading and unloading the data source resource.
      class XML_DATA_SOURCE_API XMLDataSource : public DataSource
      {
      public:
      
      ...
      
          // Add functions for loading and unloading the data source resource.
          virtual void loadFromKZB(const ResourceLoaderThreadContext* threadContext, KzcInputStream* inputStream, const KzuBinaryFileInfo* file) KZ_OVERRIDE;
          virtual void unloadOverride() KZ_OVERRIDE;
      
      ...
      
      };
    3. In the protected section of the XMLDataSource class after the initialize() function create the helper function for loading the XML file and add the function for opening the XML file after loading the data source.
      class XML_DATA_SOURCE_API XMLDataSource : public DataSource
      {
      
      ...
      
      protected:
      
      ...
      
          // Create the helper function for loading the XML file.
          void loadXmlFile(const char* filename);
      
          // Add the function for opening the XML file after loading the data source.
          virtual void loadFromKZB(Domain* domain, KzbFile& kzbFile, ReadOnlyMemoryFile& file, KzbMemoryParser& parser) KZ_OVERRIDE;
      
      ...
      };
    4. In Visual Studio add to the xmldatasource.cpp file:
      1. Include the header file required to process XML.
        // Provides the functionality to process XML.
        #include "tinyxml2.h"
      2. After the include section define the metadata for the property type which you created in the xmldatasource.hpp header file. You use this property to tell the data source plugin from which XML file to get the data.
        // Define the metadata for the property type you use to tell the data source plugin which XML file to read.
        PropertyType<string> XMLDataSource::XmlFilenameProperty(kzMakeFixedString("XMLdatasource.XMLDataSourceFile"), "", 0, false,
                                                                KZ_DECLARE_EDITOR_METADATA
                                                                (
                                                                    // Set the name of the property the way it is shown in Kanzi Studio.
                                                                    metadata.displayName = "XML Data Source File";
                                                                    // Set the tooltip for the property.
                                                                    metadata.tooltip = "Sets which XML file the data source plugin reads.";
                                                                    // Select the editor which is used to edit the value of this property type.
                                                                    // BrowseFileTextEditor editor contains a text box with a Browse button next to it.
                                                                    metadata.editor = "BrowseFileTextEditor";
                                                                ));
      3. After defining the metadata create a function which creates data objects based on the type specified in the type attribute in the XML element of the data source file.
        // Add a data object of the type specified by the type attribute in the XML element. Get the initial value from the text parameter.
        DataObjectSharedPtr addDataObject(Domain *domain, const char* type, const char* name, const char* text)
        {
            shared_ptr<DataObject> object;
            // Create an integer data object from the int type attributes.
            if (type && strcmp(type, "int") == 0)
            {
                int value = 0;
                if (text)
                {
                    value = atoi(text);
                }
                object = make_shared<DataObjectInt>(domain, name, value);
            }
            // Create a float data object from the float and real type attributes.
            else if (type && (strcmp(type, "real") == 0 || strcmp(type, "float") == 0))
            {
                double value = 0;
                if (text)
                {
                    value = atof(text);
                }
                object = make_shared<DataObjectReal>(domain, name, value);
            }
            // Create a Boolean data object from the bool and boolean type attributes.
            else if (type && (strcmp(type, "bool") == 0 || strcmp(type, "boolean") == 0))
            {
                bool value = false;
                if (text)
                {
                    value = (strcmp(text, "true") == 0);
                }
                object = make_shared<DataObjectBool>(domain, name, value);
            }
            // Create a string data object from the string type attributes.
            else if (type && strcmp(type, "string") == 0)
            {
                string value;
                if (text)
                {
                    value = text;
                }
                object = make_shared<DataObjectString>(domain, name, value);
            }
            else
            {
                // If the type attribute is not set, create a generic data object. This is used to create the hierarchy in the data source.
                object = make_shared<DataObject>(domain, name);
            }
            return object;
        }
      4. Create a function which converts the content of the XML structure in the memory to data objects.
        // This function converts the content of the XML structure in memory to data objects. You use these data objects to construct the data object tree of the data source.
        // The second parameter sets the node where this pass places new data objects.
        // The third parameter sets the pointer to the location where the conversion and parsing is progressing (within the XML in the memory, the child element in the XML).
        static void addDataObjectsRecursively(Domain* domain, DataObjectSharedPtr parent, const tinyxml2::XMLElement* xml)
        {
            // Check whether the current element in the XML file has the type attribute set.
            const tinyxml2::XMLAttribute* typeAttribute = xml->FindAttribute("type");
        
            // Get value of the type attribute.
            const char* type = 0;
            if (typeAttribute)
            {
                type = typeAttribute->Value();
            }
        
            // Create the data object based on the value of the type attribute.
            DataObjectSharedPtr object = addDataObject(domain, type, xml->Name(), xml->GetText());
            // Add the data object as a child to the parent data object.
            parent->addChild(object);
        
            // Traverse the tree in the XML file to add data objects for each child element of the current XML element.
            for (const tinyxml2::XMLElement* child = xml->FirstChildElement(); child; child = child->NextSiblingElement())
            {
                // Recurse.
                addDataObjectsRecursively(domain, object, child);
            }
        }
      5. Create the function which loads the XML file you set with the XML Data Source File property from disk to memory.
        // Create the function which loads the XML file from the disk to the memory.
        void XMLDataSource::loadXmlFile(const char* filename)
        {
            // Clear the previous data object tree.
            m_root.reset();
        
            // Load the XML file.
            // Introduce the XML to the memory.
            tinyxml2::XMLDocument doc;
            // Load the file from the disk to the memory.
            tinyxml2::XMLError error;
            unique_ptr<File> file(new ReadOnlyDiskFile(filename));
            file->seek(File::SeekBegin, 0);
            vector<char> data(static_cast<unsigned int>(file->size()));
            file->read(data.data(), data.size());
        
            // Parse the XML document from the memory and release the open file.
            error = doc.Parse(data.data(), data.size());
            file.reset();
        
            // If the plugin successfully loads the file set in the XML Data Source File property, create data objects.
            if (error == tinyxml2::XML_SUCCESS)
            {
                // Create the root node.
                const tinyxml2::XMLElement* element = doc.RootElement();
                // Create the root data object.
                m_root = make_shared<DataObject>(getDomain(), "Root");
                do
                {
                    // Populate the rest of the nodes as the child data object nodes of the root node data object.
                    addDataObjectsRecursively(getDomain(), m_root, element);
                    // Handle all siblings of the root element.
                    element = element->NextSiblingElement();
                } while (element);
            }
        }
      6. Load the data source resource and create the data object tree.
        // Load the data source resource and create the data object tree during the loading of the kzb file of the application.
        void XMLDataSource::loadFromKZB(const ResourceLoaderThreadContext* threadContext, KzcInputStream* inputStream, const KzuBinaryFileInfo* file)
        {
            // Call the base class function for loading the kzb file.
            DataSource::loadFromKZB(threadContext, inputStream, file);
        
            // If you do not set the value of the XML Data Source File property, the plugin does not do anything.
            string filename = getProperty(XmlFilenameProperty);
            if (!filename.empty())
            {
                // Call the member function that loads the XML file.
                loadXmlFile(filename.c_str());
            }
        }
      7. Call the unloadOverride() function when you exit the application or reload the kzb files in your application.
        // Kanzi calls unloadOverride() when the application exits, or when you reload kzb files in your application.
        void XMLDataSource::unloadOverride()
        {
            // Call the base class function for unloading.
            DataSource::unloadOverride();
            // Clean up the data objects used in the application.
            m_root.reset();
        }
      8. Open the XML file after loading the data source.
        // Open the XML file after loading the data source.
        void XMLDataSource::loadFromKZB(Domain* domain, KzbFile& kzbFile, ReadOnlyMemoryFile& file, KzbMemoryParser& parser)
        {
            DataSource::loadFromKZB(domain, kzbFile, file, parser);
        
            // If you do not set the value of the XML Data Source File property, the plugin does not do anything.
            string filename = getProperty(XmlFilenameProperty);
            if (!filename.empty())
            {
                // Call the member function that loads the XML file.
                loadXmlFile(filename.c_str());
            }
        }

Update the data source

This section demonstrates how you can update a data source by checking every second whether the file you use as a source of data has changed. Note that this implementation works only on Windows. The correct way to refresh a data source depends on your target platform.

To update the data source:

  1. Make these changes to the xmldatasource.hpp:
    1. In the public section of the XMLDataSource class after the definition of the create function add
      public:
      
      ...
      
          // Destructor
          virtual ~XMLDataSource() KZ_OVERRIDE
          {
              // Stop the file tracking when the user renames the data source in a Kanzi Studio project.
              stopFileTracking();
          }
    2. In the private section of the XMLDataSource class introduce the functions which you use to track the changes in the data source, and the file timestamp and the subscription token for the timer.
      Replace
      private:
      
          // Introduce the pointer to the root data object of the data source.
          DataObjectSharedPtr m_root;
      
      with
      private:
      
          // Declare the function that starts the timer which tracks the changes in the file. 
          void startFiletracking(const char* filename);
          // Declare the function that stops the timer which tracks the changes in the file.
          void stopFileTracking();
          // Declare the timer callback function.
          void onTimer(const TimerMessageArguments& arguments);
      
          // Introduce the pointer to the root data object of the data source.
          DataObjectSharedPtr m_root;
      
          // Define a member variable for the previous timestamp of the file.
          time_t m_fileTime;
          // Define a member variable for the timer subscription token.
          TimerSubscriptionToken m_timerSubscription;
  2. Make these changes to the xmldatasource.cpp:
    1. Include the header files used by the functions that refresh the data source
      // Provides the system utility function to get the timestamp of the file.
      #include <sys/stat.h>
    2. After the definition of the metadata used in the plugin add the functions that track changes in the file you use as a data source.
      // Helper function to get the file modification time.
      static time_t getFileModificationTime(const char* filename)
      {
          time_t result = 0;
      
          struct stat fs;
          if (stat(filename, &fs) == 0)
          {
              result = fs.st_mtime;
          }
      
          return result;
      }
      
      // Start the file tracking timer.
      void XMLDataSource::startFiletracking(const char* filename)
      {
          stopFileTracking();
      
          m_fileTime = getFileModificationTime(filename);
      
          // Start the timer that sets off every second until you stop it, and calls the onTimer() callback function.
          m_timerSubscription = addTimerHandler(getMessageDispatcher(), chrono::seconds(1), KZU_TIMER_MESSAGE_MODE_REPEAT_BATCH, 
              bind(&XMLDataSource::onTimer, this, placeholders::_1));
      }
      
      // Stop the file tracking timer.
      void XMLDataSource::stopFileTracking()
      {
          if (m_timerSubscription)
          {
              // Remove the timer.
              removeTimerHandler(getMessageDispatcher(), m_timerSubscription);
              // Clear the subscription handler.
              m_timerSubscription.reset();
          }
      }
       
      // Timer callback function.
      void XMLDataSource::onTimer(const TimerMessageArguments& /*arguments*/)
      {
          string filename = getProperty(XmlFilenameProperty);
          // If you do not set the value of the XML Data Source File property, the plugin does not do anything.
          if (!filename.empty())
          {
              // Get the modification time.
              time_t modificationTime = getFileModificationTime(filename.c_str());
              // Compare the current and previous modification times. If they differ, update the file.
              if (modificationTime != m_fileTime)
              {
                  loadXmlFile(filename.c_str());
      
                  m_fileTime = modificationTime;
      
                  // Notify that the data source has changed. This assumes that everything has changed.
                  notifyModified();
              }
          }
      }
    3. Change the loadXmlFile() function to include the functionality for tracking the changes in the data source file.
      // Create the function which loads the XML file from disk to memory.
      void XMLDataSource::loadXmlFile(const char* filename)
      {
          stopFileTracking();
      
      ...
      
          if (error == tinyxml2::XML_SUCCESS)
          {
      
      ...
      
              startFiletracking(filename);
          }
      
      ...
      }
    4. Add to the end of the XMLDataSource::unloadOverride() function a call to the stopFileTracking() function to stop the file tracking.
      void XMLDataSource::unloadOverride()
      {
      
      ...
      
      
          stopFileTracking();
      }

Build the data source plugin

After you are done creating the plugin, build the plugin .dll which you use in the next step of this tutorial in the Kanzi Studio project to get the data for your application from an XML file.

To build the plugin in Visual Studio in the Solution Explorer right-click the XML_data_source project and select Build.


< INTRODUCTION
NEXT STEP >